from typing import Callable, Container, Iterable, List, NamedTuple, NewType, Optional, Tuple, TypeVar

from thefuzz import fuzz
from tree_sitter import Node


class NoSuchChild(Exception):
    def __init__(self, *, name: Optional[str] = None, id: Optional[int] = None):
        if name is not None:
            super().__init__(f"no child with field name {name}")
        if id is not None:
            super().__init__(f"no child with index {id}")


class NoText(Exception):
    def __init__(self):
        super().__init__(f"no text")


CNode = NewType("CNode", Node)
RustNode = NewType("RustNode", Node)
N = TypeVar("N", bound=Node)


def unsafe_child_by_name(node: N, field_name: str) -> N:
    """
    Raises:
        NoSuchChild: if there is no such child with the specified field name
    """
    child = node.child_by_field_name(field_name)
    if child is None:
        raise NoSuchChild(name=field_name)
    return child


def unsafe_child_by_index(node: N, index: int) -> N:
    """
    Raises:
        NoSuchChild: if the index is out of range
    """
    child = node.child(index)
    if child is None:
        raise NoSuchChild(id=index)
    return child


def unsafe_text(node: Node) -> str:
    if node.text is None:
        raise NoText()
    return node.text.decode("utf-8")


def unsafe_child_text(node: Node, field_name: str) -> str:
    """
    Raises:
        NoSuchChild
        NoText
    """
    return unsafe_text(unsafe_child_by_name(node, field_name))


def unsafe_declarator_name(node: Node) -> str:
    """
    Raises:
        NoSuchChild
        NoText
    """
    while True:
        if (_tmp_node := node.child_by_field_name("declarator")) is not None:
            node = _tmp_node
        elif node.type == "parenthesized_declarator":
            node = unsafe_child_by_index(node, 1)
        else:
            break
    return unsafe_text(node)


class MatchData(NamedTuple):
    rust_nodes: List[RustNode]
    names: List[str]
    text: str


def merge_matches(matches: Iterable[MatchData]) -> Optional[MatchData]:

    rust_nodes = []
    names = []
    texts = []
    count = 0
    for m in matches:
        rust_nodes.extend(m.rust_nodes)
        names.extend(m.names)
        texts.append(m.text)
        count += 1
    if count == 0:
        return None
    return MatchData(rust_nodes, names, "\n".join(texts))


def best_match_in_col(
    aim: str, col: Iterable[N], col_key, compare_func: Callable[[str, str], int], threshold: int
) -> Optional[N]:
    matched: List[Tuple[float, N]] = []
    for c in col:
        try:
            ck = col_key(c)
        except (NoSuchChild, NoText):
            continue
        similarity = compare_func(aim, ck)
        if similarity >= threshold:
            matched.append((similarity, c))
    if len(matched) > 0:
        best_match = max(matched, key=lambda x: x[0])
        return best_match[1]
    else:
        return None


def _match_similarly(
    name: Optional[str], rust_nodes: Iterable[RustNode], kinds: Container[str], threshold: int, convert_name: Callable[[str], str]
) -> Optional[MatchData]:
    candidates: List[RustNode] = [item for item in rust_nodes if item.type in kinds]
    if not name:
        return None
    name = convert_name(name)

    best_match = best_match_in_col(name, candidates, lambda c: convert_name(unsafe_child_text(c, "name")), fuzz.ratio, threshold)
    if best_match is None:
        return None
    try:
        rust_name = unsafe_child_text(best_match, "name")
        return MatchData([best_match], [rust_name], unsafe_text(best_match))
    except NoText:
        return None


def match_similarly(
    name: Optional[str], rust_nodes: Iterable[RustNode], kinds: Container[str], threshold: int = 90
) -> Optional[MatchData]:
    return _match_similarly(name, rust_nodes, kinds, threshold, lambda x: x)


def match_similarly_lower(
    name: Optional[str], rust_nodes: Iterable[RustNode], kinds: Container[str], threshold: int = 90
) -> Optional[MatchData]:
    return _match_similarly(name, rust_nodes, kinds, threshold, lambda x: x.lower())


def match_exactly(name: Optional[str], rust_nodes: Iterable[RustNode], kinds: Container[str]) -> Optional[MatchData]:
    return match_similarly(name, rust_nodes, kinds, 100)


def match_exactly_lower(name: Optional[str], rust_nodes: Iterable[RustNode], kinds: Container[str]) -> Optional[MatchData]:
    return match_similarly_lower(name, rust_nodes, kinds, 100)


def match_similar_str(str1: Optional[str], str2: Optional[str], threshold: int):
    if not str1 or not str2:
        return False
    return fuzz.ratio(str1, str2) >= 90


def match_exact_str(str1: Optional[str], str2: Optional[str]):
    if not str1 or not str2:
        return False
    return str1 == str2


def match_similar_lower_str(str1: Optional[str], str2: Optional[str], threshold: int):
    if not str1 or not str2:
        return False
    return fuzz.ratio(str1.lower(), str2.lower()) >= 90


def match_exact_lower_str(str1: Optional[str], str2: Optional[str]):
    if not str1 or not str2:
        return False
    return str1.lower() == str2.lower()
